import { WithDisposable } from '@notes/lit';
import { isSameDay, isSameMonth, isToday } from 'date-fns';
import {
    html,
    LitElement,
    nothing,
    type PropertyValues,
    type TemplateResult,
} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';

import { arrowLeftIcon } from './icons.js';
import { datePickerStyle } from './style.js';
import { clamp, getMonthMatrix, toDate } from './utils.js';

const days = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
const months = [
    'Jan',
    'Feb',
    'Mar',
    'Apr',
    'May',
    'Jun',
    'Jul',
    'Aug',
    'Sep',
    'Oct',
    'Nov',
    'Dec',
];

export interface DateCell {
    date: Date;
    label: string;
    isToday: boolean;
    notCurrentMonth: boolean;
    selected?: boolean;
    tabIndex?: number;
}

type NavActionArg = {
    action: () => void;
    disable?: boolean;
};

/**
 * Date picker
 */
@customElement('date-picker')
export class DatePicker extends WithDisposable(LitElement) {
    static override styles = datePickerStyle;

    /** Checked date timestamp */
    @property({ type: Number })
    value?: number;

    @property({ attribute: false })
    onChange?: (value: Date) => void;

    /** card padding in px */
    @property({ type: Number })
    padding = 20;
    /** cell size in px */
    @property({ type: Number })
    size = 28;
    /** horizontal gap between cells in px */
    @property({ type: Number })
    gapH = 10;
    /** vertical gap between cells in px */
    @property({ type: Number })
    gapV = 8;

    /** current active month */
    private _cursor = new Date();
    /** web-accessibility for month select */
    @property({ attribute: false })
    private _monthCursor = 0;
    @property({ attribute: false })
    private _yearCursor = 0;
    @property({ attribute: false })
    private _monthPickYearCursor = 0;
    /** date matrix */
    @property({ attribute: false })
    private _matrix: DateCell[][] = [];
    @property({ attribute: false })
    private _yearMatrix: number[] = [];
    @property({ attribute: false })
    private _mode: 'date' | 'month' | 'year' = 'date';

    private _maxYear = 2099;
    private _minYear = 1970;

    get year() {
        return this._cursor.getFullYear();
    }
    get month() {
        return this._cursor.getMonth();
    }
    get date() {
        return this._cursor.getDate();
    }
    get day() {
        return this._cursor.getDay();
    }
    get yearLabel() {
        return this.year;
    }
    get monthLabel() {
        return months[this.month];
    }
    get dayLabel() {
        return days[this.day];
    }

    get cardWidth() {
        const colNum = 7;
        return this.size * colNum + this.padding * 2 + this.gapH * (colNum - 1);
    }
    get cardHeight() {
        const rowNum = 7;
        return this.size * rowNum + this.padding * 2 + this.gapV * (rowNum - 1) - 2;
    }
    get minHeight() {
        const rowNum = 8;
        return this.size * rowNum + this.padding * 2 + this.gapV * (rowNum - 1) - 2;
    }

    get _cardStyle() {
        return {
            '--cell-size': `${this.size}px`,
            '--gap-h': `${this.gapH}px`,
            '--gap-v': `${this.gapV}px`,
            'min-width': `${this.cardWidth}px`,
            'min-height': `${this.cardHeight}px`,
            padding: `${this.padding}px`,
        };
    }

    /**
     * Focus on date-cell
     */
    public focusDateCell() {
        const lastEl = this.shadowRoot?.querySelector(
            'button.date-cell[tabindex="0"]'
        ) as HTMLElement;
        lastEl?.focus();
    }

    /**
     * check if date-cell is focused
     * @returns
     */
    public isDateCellFocused() {
        const focused = this.shadowRoot?.activeElement as HTMLElement;
        return focused?.classList.contains('date-cell');
    }

    public focusMonthCell() {
        const lastEl = this.shadowRoot?.querySelector(
            'button.month-cell[tabindex="0"]'
        ) as HTMLElement;
        lastEl?.focus();
    }

    public isMonthCellFocused() {
        const focused = this.shadowRoot?.activeElement as HTMLElement;
        return focused?.classList.contains('month-cell');
    }

    public focusYearCell() {
        const lastEl = this.shadowRoot?.querySelector(
            'button.year-cell[tabindex="0"]'
        ) as HTMLElement;
        lastEl?.focus();
    }

    public isYearCellFocused() {
        const focused = this.shadowRoot?.activeElement as HTMLElement;
        return focused?.classList.contains('year-cell');
    }

    public openMonthSelector() {
        this._monthCursor = this.month;
        this._monthPickYearCursor = this.year;
        this._mode = 'month';
    }
    public closeMonthSelector() {
        this._mode = 'date';
    }
    public toggleMonthSelector() {
        if (this._mode === 'month') this.closeMonthSelector();
        else this.openMonthSelector();
    }
    public openYearSelector() {
        this._yearCursor = clamp(this._minYear, this._maxYear, this.year);
        this._mode = 'year';
        this._getYearMatrix();
    }
    public closeYearSelector() {
        this._mode = 'date';
    }
    public toggleYearSelector() {
        if (this._mode === 'year') this.closeYearSelector();
        else this.openYearSelector();
    }

    private _moveMonth(offset: number) {
        this._cursor.setMonth(this._cursor.getMonth() + offset);
        this._getMatrix();
    }
    private _modeDecade(offset: number) {
        this._yearCursor = clamp(
            this._minYear,
            this._maxYear,
            this._yearCursor + offset
        );
        this._getYearMatrix();
    }

    private _onChange(date: Date, emit = true) {
        this._cursor = date;
        this.value = date.getTime();
        this._getMatrix();
        emit && this.onChange?.(date);
    }

    private _getMatrix() {
        this._matrix = getMonthMatrix(this._cursor).map(row => {
            return row.map(date => {
                const tabIndex = isSameDay(date, this._cursor) ? 0 : -1;
                return {
                    date,
                    label: date.getDate().toString(),
                    isToday: isToday(date),
                    notCurrentMonth: !isSameMonth(date, this._cursor),
                    selected: this.value ? isSameDay(date, toDate(this.value)) : false,
                    tabIndex,
                } satisfies DateCell;
            });
        });
    }

    private _getYearMatrix() {
        // every decade has 12 years
        const no = Math.floor((this._yearCursor - this._minYear) / 12);
        const decade = no * 12;
        const start = this._minYear + decade;
        const end = start + 12;
        this._yearMatrix = Array.from(
            { length: end - start },
            (_, i) => start + i
        ).filter(v => v >= this._minYear && v <= this._maxYear);
    }

    override firstUpdated(): void {
        this._disposables.addFromEvent(
            this,
            'keydown',
            e => {
                e.stopPropagation();
                const directions = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
                if (directions.includes(e.key) && this.isDateCellFocused()) {
                    e.preventDefault();

                    if (e.key === 'ArrowLeft') {
                        this._cursor.setDate(this._cursor.getDate() - 1);
                    } else if (e.key === 'ArrowRight') {
                        this._cursor.setDate(this._cursor.getDate() + 1);
                    } else if (e.key === 'ArrowUp') {
                        this._cursor.setDate(this._cursor.getDate() - 7);
                    } else if (e.key === 'ArrowDown') {
                        this._cursor.setDate(this._cursor.getDate() + 7);
                    }
                    this._getMatrix();
                    setTimeout(this.focusDateCell.bind(this));
                }

                if (directions.includes(e.key) && this.isMonthCellFocused()) {
                    e.preventDefault();
                    if (e.key === 'ArrowLeft') {
                        this._monthCursor = (this._monthCursor - 1 + 12) % 12;
                    } else if (e.key === 'ArrowRight') {
                        this._monthCursor = (this._monthCursor + 1) % 12;
                    } else if (e.key === 'ArrowUp') {
                        this._monthCursor = (this._monthCursor - 3 + 12) % 12;
                    } else if (e.key === 'ArrowDown') {
                        this._monthCursor = (this._monthCursor + 3) % 12;
                    }
                    setTimeout(this.focusMonthCell.bind(this));
                }

                if (directions.includes(e.key) && this.isYearCellFocused()) {
                    e.preventDefault();
                    if (e.key === 'ArrowLeft') {
                        this._modeDecade(-1);
                    } else if (e.key === 'ArrowRight') {
                        this._modeDecade(1);
                    } else if (e.key === 'ArrowUp') {
                        this._modeDecade(-3);
                    } else if (e.key === 'ArrowDown') {
                        this._modeDecade(3);
                    }
                    setTimeout(this.focusYearCell.bind(this));
                }

                if (e.key === 'Tab') {
                    setTimeout(() => {
                        const focused = this.shadowRoot?.activeElement as HTMLElement;
                        const firstEl = this.shadowRoot?.querySelector('button');

                        // check if focus the last element, then focus the first element
                        if (!e.shiftKey && !focused) firstEl?.focus();
                        // check if focused element is inside current date-picker
                        if (e.shiftKey && !this.shadowRoot?.contains(focused))
                            this.focusDateCell();
                    });
                }
            },
            true
        );
    }

    private _switchMode<T>(map: Record<typeof this._mode, T>) {
        return (map[this._mode] as T) ?? nothing;
    }

    override updated(_changedProperties: PropertyValues): void {
        if (_changedProperties.has('value')) {
            // this._getMatrix();
            if (this.value) this._onChange(toDate(this.value), false);
            else this._getMatrix();
        }
    }

    override connectedCallback(): void {
        super.connectedCallback();
        if (this.value) this._cursor = toDate(this.value);
        this._getMatrix();
    }

    /** Actions */
    private _navAction(
        prev: NavActionArg | NavActionArg['action'],
        curr: NavActionArg | NavActionArg['action'],
        slot?: TemplateResult
    ) {
        const onPrev = typeof prev === 'function' ? prev : prev.action;
        const onNext = typeof curr === 'function' ? curr : curr.action;
        const prevDisable = typeof prev === 'function' ? false : prev.disable;
        const nextDisable = typeof curr === 'function' ? false : curr.disable;
        const classes = classMap({
            'date-picker-header__action': true,
            'with-slot': !!slot,
        });
        return html`<div class=${classes}>
      <button
        aria-label="previous month"
        class="date-picker-small-action interactive left"
        @click=${onPrev}
        ?disabled=${prevDisable}
      >
        ${arrowLeftIcon}
      </button>
      ${slot ?? nothing}
      <button
        aria-label="next month"
        class="date-picker-small-action interactive right"
        @click=${onNext}
        ?disabled=${nextDisable}
      >
        ${arrowLeftIcon}
      </button>
    </div>`;
    }

    /** Week header */
    private _dayHeaderRenderer() {
        return html`<div class="days-header">
      ${days.map(day => html`<div class="date-cell">${day}</div>`)}
    </div>`;
    }

    /** Cell */
    private _cellRenderer(cell: DateCell) {
        const classes = classMap({
            interactive: true,
            'date-cell': true,
            'date-cell--today': cell.isToday,
            'date-cell--not-curr-month': cell.notCurrentMonth,
            'date-cell--selected': !!cell.selected,
        });
        const dateRaw = `${cell.date.getFullYear()}-${cell.date.getMonth()}-${cell.date.getDate()}(${cell.date.getDay()})`;
        return html`<button
      tabindex=${cell.tabIndex ?? -1}
      aria-label=${dateRaw}
      data-date=${dateRaw}
      class=${classes}
      @click=${() => {
                this._onChange(cell.date);
            }}
    >
      ${cell.label}
    </button>`;
    }

    private _dateContent() {
        return html` <div class="date-picker-header">
        <div class="date-picker-header__buttons">
          <button
            class="date-picker-header__date interactive"
            @click=${() => this.toggleMonthSelector()}
          >
            <div>${this.monthLabel}</div>
          </button>

          <button
            class="date-picker-header__date interactive"
            @click=${() => this.toggleYearSelector()}
          >
            <div>${this.yearLabel}</div>
          </button>
        </div>

        ${this._navAction(
            () => this._moveMonth(-1),
            () => this._moveMonth(1),
            html`<button
            tabindex="0"
            aria-label="today"
            class="action-label interactive today"
            @click=${() => {
                    this._onChange(new Date());
                }}
          >
            <span>TODAY</span>
          </button>`
        )}
      </div>
      ${this._dayHeaderRenderer()}
      <div class="date-picker-weeks">
        ${this._matrix.map(
            week =>
                html`<div class="date-picker-week">
              ${week.map(cell => this._cellRenderer(cell))}
            </div>`
        )}
      </div>`;
    }

    private _monthContent() {
        return html` <div class="date-picker-header">
        <button
          class="date-picker-header__date interactive"
          @click=${() => this.toggleMonthSelector()}
        >
          <div>${this._monthPickYearCursor}</div>
        </button>

        ${this._navAction(
            {
                action: () => this._monthPickYearCursor--,
                disable: this._monthPickYearCursor <= this._minYear,
            },
            {
                action: () => this._monthPickYearCursor++,
                disable: this._monthPickYearCursor >= this._maxYear,
            }
        )}
      </div>
      <div class="date-picker-month">
        ${months.map((month, index) => {
            const isActive = this.value
                ? isSameMonth(
                    this.value,
                    new Date(this._monthPickYearCursor, index, 1)
                )
                : false;
            const classes = classMap({
                'month-cell': true,
                interactive: true,
                active: isActive,
            });
            return html`<button
            tabindex=${this._monthCursor === index ? 0 : -1}
            aria-label=${month}
            class=${classes}
            @click=${() => {
                    this._cursor.setMonth(index);
                    this._cursor.setFullYear(this._monthPickYearCursor);
                    this._mode = 'date';
                    this._getMatrix();
                }}
          >
            ${month}
          </button>`;
        })}
      </div>`;
    }

    private _yearContent() {
        const startYear = this._yearMatrix[0];
        const endYear = this._yearMatrix[this._yearMatrix.length - 1];
        return html`<div class="date-picker-header">
        <button
          class="date-picker-header__date interactive"
          @click=${() => this.toggleYearSelector()}
        >
          <div>${startYear}-${endYear}</div>
        </button>
        ${this._navAction(
            {
                action: () => this._modeDecade(-12),
                disable: startYear <= this._minYear,
            },
            {
                action: () => this._modeDecade(12),
                disable: endYear >= this._maxYear,
            }
        )}
      </div>
      <div class="date-picker-year">
        ${this._yearMatrix.map(year => {
            const isActive = year === this._cursor.getFullYear();
            const classes = classMap({
                'year-cell': true,
                interactive: true,
                active: isActive,
            });
            return html`<button
            tabindex=${this._yearCursor === year ? 0 : -1}
            aria-label=${year}
            class=${classes}
            @click=${() => {
                    this._cursor.setFullYear(year);
                    this._mode = 'date';
                    this._getMatrix();
                }}
          >
            ${year}
          </button>`;
        })}
      </div>`;
    }

    override render() {
        const classes = classMap({
            'date-picker': true,
            [`date-picker--mode-${this._mode}`]: true,
        });
        const wrapperStyle = styleMap({
            'min-height': `${this.minHeight}px`,
        });
        return html`<div style=${wrapperStyle} class="date-picker-height-wrapper">
      <div class=${classes} style=${styleMap(this._cardStyle)}>
        ${this._switchMode({
            date: this._dateContent(),
            month: this._monthContent(),
            year: this._yearContent(),
        })}
      </div>
    </div>`;
    }
}

declare global {
    interface HTMLElementTagNameMap {
        'date-picker': DatePicker;
    }
}
